/*
 * Copyright 1999-2004 Carnegie Mellon University.  
 * Portions Copyright 2002-2004 Sun Microsystems, Inc.  
 * Portions Copyright 2002-2004 Mitsubishi Electric Research Laboratories.
 * All Rights Reserved.  Use is subject to license terms.
 * 
 * See the file "license.terms" for information on usage and
 * redistribution of this file, and for a DISCLAIMER OF ALL 
 * WARRANTIES.
 *
 */

package edu.cmu.sphinx.tools.audio;

import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.Arrays;

/** Provides an interface to view and play back various forms of an audio signal. */
@SuppressWarnings("serial")
public class AudioPanel extends JPanel
        implements MouseMotionListener, MouseListener {

    private final AudioData audio;
    private float[] labelTimes;
    private String[] labels;
    private float xScale;
    private final float yScale;
    private final float originalXScale;
    private int xDragStart;
    private int xDragEnd;
    protected int selectionStart = -1;
    protected int selectionEnd = -1;


    /**
     * Creates a new AudioPanel.  The scale factors represent how much to scale the audio.  A scaleX factor of 1.0f
     * means one pixel per sample, and a scaleY factor of 1.0f means one pixel per resolution of the sample (e.g., a
     * scale of 1.0f would take 2**16 pixels).
     *
     * @param audioData the AudioData to draw
     * @param scaleX    how much to scale the width of the audio
     * @param scaleY    how much to scale the height
     */
    public AudioPanel(AudioData audioData,
                      float scaleX,
                      float scaleY) {
        this.audio = audioData;
        labelTimes = new float[0];
        labels = new String[0];
        this.xScale = scaleX;
        this.yScale = scaleY;
        this.originalXScale = this.xScale;

        int width = (int) (audio.getAudioData().length * xScale);
        int height = (int) ((1 << 16) * yScale);

        setPreferredSize(new Dimension(width, height));
        setBackground(Color.white);

        audio.addChangeListener(new ChangeListener() {
            public void stateChanged(ChangeEvent event) {
                int width = (int) (audio.getAudioData().length * xScale);
                int height = (int) ((1 << 16) * yScale);

                labelTimes = new float[0];
                labels = new String[0];

                setSelectionStart(-1);
                setSelectionEnd(-1);

                setPreferredSize(new Dimension(width, height));
                Dimension sz = getSize();

                revalidate();
                repaint(0, 0, 0, sz.width, sz.height);
            }
        });

        addMouseMotionListener(this);
        addMouseListener(this);
        setFocusable(true);
        requestFocus();
    }


    /** Sets the labels to be used when drawing this panel.
      * @param labelTimes label times
      * @param labels labels
      */
    public void setLabels(float[] labelTimes, String[] labels) {
        this.labelTimes = labelTimes;
        this.labels = labels;
        repaint();
    }


    /** Sets the zoom, adjusting the scroll bar in the process. 
      * @param zoom float zoom
      */
    protected void zoomSet(float zoom) {
        xScale = originalXScale * zoom;
        int width = (int) (audio.getAudioData().length * xScale);
        int height = (int) ((1 << 16) * yScale);

        setPreferredSize(new Dimension(width, height));
        revalidate();
        repaint();
    }


    /**
     * Repaints the component with the given Graphics.
     *
     * @param g the Graphics to use to repaint the component.
     */
    @Override
    public void paintComponent(Graphics g) {
        int pos, index;
        int length;

        super.paintComponent(g);

        Dimension sz = getSize();
        int gZero = sz.height / 2;
        short[] audioData = audio.getAudioData();

        /**
         * Only draw what is in the viewport.
         */
        JViewport viewport = getViewport();
        if (viewport != null) {
            Rectangle r = viewport.getViewRect();
            pos = (int) r.getX();
            length = (int) r.getWidth();
        } else {
            pos = 0;
            length = (int) (audioData.length * xScale);
        }

        /**
         * Fill in the whole image with white.
         */
        g.setColor(Color.WHITE);
        g.fillRect(pos, 0, length, sz.height - 1);

        /**
         * Now fill in the audio selection area as gray.
         */
        index = Math.max(0, getSelectionStart());
        int start = (int) (index * xScale);
        index = getSelectionEnd();
        if (index == -1) {
            index = audioData.length - 1;
        }
        int end = (int) (index * xScale);
        g.setColor(Color.LIGHT_GRAY);
        g.fillRect(start, 0,
                end - start, sz.height - 1);

        /* Now scale the audio data and draw it.
         */
        int[] x = new int[length];
        int[] y = new int[length];
        for (int i = 0; i < length; i++) {
            x[i] = pos;
            index = (int) (pos / xScale);
            if (index < audioData.length) {
                y[i] = gZero - (int) (audioData[index] * yScale);
            } else {
                break;
            }
            pos++;
        }
        g.setColor(Color.RED);
        g.drawPolyline(x, y, length);

        /**
         * Now draw the labels.
         */
        for (int i = 0; i < labelTimes.length; i++) {
            pos = (int) (xScale
                    * labelTimes[i]
                    * audio.getAudioFormat().getSampleRate());
            g.drawLine(pos, 0, pos, sz.height - 1);
            g.drawString(labels[i], pos + 5, sz.height - 5);
        }
    }


    /** Finds the JViewport enclosing this component. */
    private JViewport getViewport() {
        Container p = getParent();
        if (p instanceof JViewport) {
            Container gp = p.getParent();
            if (gp instanceof JScrollPane) {
                JScrollPane scroller = (JScrollPane) gp;
                JViewport viewport = scroller.getViewport();
                if (viewport == null || viewport.getView() != this) {
                    return null;
                } else {
                    return viewport;
                }
            }
        }
        return null;
    }


    /**
     * Returns the index of the sample representing the start of the selection.  -1 means the very beginning.
     *
     * @return the start of the selection
     * @see #crop
     * @see #getSelectionEnd
     */
    public int getSelectionStart() {
        return selectionStart;
    }


    /**
     * Sets the index of the sample of representing the start of the selection.  -1 means the very beginning.
     *
     * @param newStart the new selection start
     * @see #crop
     * @see #setSelectionEnd
     */
    public void setSelectionStart(int newStart) {
        selectionStart = newStart;
        if (selectionEnd != -1) {
            if (selectionEnd < selectionStart) {
                selectionEnd = selectionStart;
            }
        }
    }


    /**
     * Returns the index of the sample representing the end of the selection.  -1 means the very end.
     *
     * @return the end of the selection
     * @see #crop
     * @see #getSelectionStart
     */
    public int getSelectionEnd() {
        return selectionEnd;
    }


    /**
     * Sets the index of the sample of representing the end of the selection.  -1 means the very end.
     *
     * @param newEnd the new selection end
     * @see #crop
     * @see #setSelectionStart
     */
    public void setSelectionEnd(int newEnd) {
        selectionEnd = newEnd;
        if (selectionEnd != -1) {
            if (selectionStart > selectionEnd) {
                selectionStart = selectionEnd;
            }
        }
    }


    /**
     * Crops the audio data between the start and end selections. All audio data outside the region will be permanently
     * lost. The selection will be reset to the very beginning and very end of the cropped clip.
     *
     * @see #getSelectionStart
     * @see #getSelectionEnd
     */
    public void crop() {
        short[] shorts = audio.getAudioData();
        int start = Math.max(0, getSelectionStart());
        int end = getSelectionEnd();
        if (end == -1) {
            end = shorts.length;
        }
        audio.setAudioData(Arrays.copyOfRange(shorts, start, end));

        setSelectionStart(-1);
        setSelectionEnd(-1);
    }


    /** Clears the current selection. */
    public void selectAll() {
        setSelectionStart(-1);
        setSelectionEnd(-1);
        repaint();
    }


    /**
     * When the mouse is pressed, we update the selection in the audio.
     *
     * @param evt the mouse pressed event
     */
    public void mousePressed(MouseEvent evt) {
        xDragStart = Math.max(0, evt.getX());
        setSelectionStart((int) (xDragStart / xScale));
        setSelectionEnd((int) (xDragStart / xScale));
        repaint();
    }


    /**
     * When the mouse is dragged, we update the selection in the audio.
     *
     * @param evt the mouse dragged event
     */
    public void mouseDragged(MouseEvent evt) {
        xDragEnd = evt.getX();
        if (xDragEnd < (int) (getSelectionStart() * xScale)) {
            setSelectionStart((int) (xDragEnd / xScale));
        } else {
            setSelectionEnd((int) (xDragEnd / xScale));
        }
        repaint();
    }


    public void mouseReleased(MouseEvent evt) {
    }


    public void mouseMoved(MouseEvent evt) {
    }


    public void mouseEntered(MouseEvent evt) {
    }


    public void mouseExited(MouseEvent evt) {
    }


    public void mouseClicked(MouseEvent evt) {
    }
}
